<?php 
/*
 * ImageProcessor v1.3
 * by Olivier St-Laurent
 * 
 * Simple Front-end Image Processor. 
 *      This script will process images at page load then cache it, instead of when you upload it in the cms. 
 *      This way, it's easier to change the wanted image size AFTER uploading all your images, 
 *      and also easier to make more than one size for a single image, to be shown at different places in the front-end.
 * 
 * Dependencies : PHP 5.3 with the extensions GD and EXIF enabled, Apache with rewrite module and AllowOverride
 * 
 * Usage : <img src="/original_image_path.jpg?params" />
 * 
 * Params : 
 *      type = string (jpg|png|gif)  will keep original if none given
 *      quality = (0-100)  default 90 (only for jpeg)
 *      maxw = integer (set maximum width without changing original image ratio)
 *      maxh = integer (set maximum height without changing original image ratio)
 *      minw = integer (set minimum width without changing original image ratio)
 *      minh = integer (set minimum height without changing original image ratio)
 *      size = string "640x480" (Set the exact new image size. Use with crop or margins, otherwise the image might be distorted)
 *      w = integer (set specific width, automatic height... If given, will override size param)
 *      h = integer (set specific height, automatic width... If given, will override w and size params)
 *      crop = string or no value
 *      margins = string or no value
 *              (For crop and margins, No need for a value... True if param is present, otherwise defaults to False) 
 *              (String can be given for the cropped/margins region with the following possibilities : top, left, right, topleft, topright, bottom, bottomleft, bottomright)
 *                  *** You can also give specific align position x,y relative to the final output size
 *              (if no region is defined, image will be centered)
 *      filter = string (grayscale,... Other filters will be added. Multiple can be given at once)
 *      bgcolor = string "255,255,255" (default is transparent for png otherwise white) (used as margin colors when resizing with margins)
 *      nocache (if this param is set, it will not save the file but instead it will output if directly, processing it everytime... FOR DEBUGGING PURPOSES ONLY)
 * 
 *   Text Params :
 *      text = Url Encoded String, This text will appear in the middle (vertically) of the image
 *      uppertext = Url Encoded String, This text will appear at the top of the image
 *      lowertext = Url Encoded String, This text will appear at the bottom of the image
 *        *size = Font size in pixels or size Range to fit the image's width (Ex: "10-16"). Default is "10-20".
 *        *color = string "255,255,255"
 *        *font = string "OpenSans-Bold.ttf" TTF Font FileName
 *        *align = string "center" | "left" | "right" (Default is "center")
 * 
 * 
 * Any additional param will still be used in the resulting image name so it is possible (although not recommended) to prevent caching by adding a timestamp value. 
 * If none of the params listed above are used, the original image will be returned directly without any processing. 
 * If size param is given, the min/max params are completely ignored (maxw, maxh, minw, minh)
 * If crop param is given, margins param is ignored
 * 
 *  Ex 1 : <img src="/images/logo.png?resize=200x200&margins&bgcolor=0,0,0&filter=grayscale&quality=80&type=jpg" />
 *      The above example will resize an image to exactly 200x200, adding black margins around the image if necessary to prevent image distortion, and converting to a grayscaled JPEG with a quality of 80
 * 
 *  Ex 2 : <img src="/images/logo.png?resize=200x200&crop=topright" />
 *      The above example will resize an image to exactly 200x200, cropping either the top or the right side if necessary to prevent image distortion, keeping the bottom left corner always visible
 * 
 *  Ex 3 : <img src="/images/logo.png?maxh=80" />
 *      The above example will simply make sure that the resulting image has a height of 80 pixels of less. If the input image is more than 80px of height, it will shrink it down to 80 pixels of height, also shrinking the width, keeping the original ratio. 
 */

/* .htaccess : 

    RewriteEngine On

    # ImageProcessor
    RewriteCond %{QUERY_STRING} .
    RewriteCond %{REQUEST_FILENAME}?%{QUERY_STRING} -s
    RewriteCond %{REQUEST_FILENAME}?%{QUERY_STRING} -f
    RewriteRule ^.*$ %{REQUEST_FILENAME}\%3F%{QUERY_STRING} [NC,L]
    RewriteCond %{QUERY_STRING} .
    RewriteCond %{REQUEST_FILENAME} -s
    RewriteCond %{REQUEST_FILENAME} -f
    RewriteCond %{REQUEST_FILENAME} \.(jpg|JPG|jpeg|JPEG|png|PNG|gif|GIF)$
    RewriteRule ^.*$ /imageprocessor.php [NC,L,QSA]


    # Zend rewrite rules here, if using Zend....

 */


// Error Reporting
ini_set('display_errors', true); // Set to True for debugging
error_reporting(E_ALL);
ini_set('memory_limit', '512M');


// Functions
function getImageTypeFromSrc($src) {
    switch (exif_imagetype($src)) {
        case IMAGETYPE_JPEG : case IMAGETYPE_JPEG2000 :
            return 'jpeg';
        case IMAGETYPE_PNG :
            return 'png';
        case IMAGETYPE_GIF : case IMAGETYPE_ICO :
            return 'gif';
        default : return false;
    }
}
function imageCreateFromSrc($src) {
    $type = getImageTypeFromSrc($src);
    if ($type) {
        $func = "imagecreatefrom".$type;
        return $func($src);
    }
}
function getTextBoundingBox($text, $font, $fontSize) {
    $width = 0;
    $height = 0;
    foreach (preg_split("#\n#", $text) as $line) { // Multiline
        if (strlen(trim($line)) > 0) {
            $boudingBox = imagettfbbox($fontSize, 0, $font , $line);
            $farRight = $boudingBox[2] > $boudingBox[4] ? $boudingBox[2] : $boudingBox[4];
            $farLeft = $boudingBox[0] < $boudingBox[6] ? $boudingBox[0] : $boudingBox[6];
            $farTop = $boudingBox[7] < $boudingBox[5] ? $boudingBox[7] : $boudingBox[5];
            $farBottom = $boudingBox[1] > $boudingBox[3] ? $boudingBox[1] : $boudingBox[3];
            $w = $farRight - $farLeft;
            $h = $farBottom - $farTop;
            // Add up totals for each line into a single bounding box
            if ($w > $width) {
                $width = $w;
            }
            $height += $h + 2;
        }
    }
    return array(
        'width' => $width,
        'height' => $height,
    );
}
function addTextToImage($image, $text, $font, $fontSize, $color, $x, $y, $width, $align = 'center') {
    foreach (preg_split("#\n#", $text) as $line) {
        if (strlen(trim($line)) > 0) {
            $lineBoundingBox = getTextBoundingBox($line, $font, $fontSize);
            // Align Text Horizontally
            switch ($align) {
                case 'center' : default : 
                    $posX = $x + ($width - $lineBoundingBox['width']) / 2;
                    break;
                case 'left' : 
                    $posX = $x;
                    break;
                case 'right' : 
                    $posX = $x + $width - $lineBoundingBox['width'];
                    break;
            }
            $baselineY = $y + $fontSize;
            $textAngle = 0;
            imagettftext($image, $fontSize, $textAngle, $posX, $baselineY, imagecolorallocate($image, $color[0], $color[1], $color[2]), $font, $line);
            $y += $lineBoundingBox['height'] + 2; // Add-up current text height to next line's vertical position
        }
    }
}


// Initialize input/output paths information
$fileurl = strstr($_SERVER['REQUEST_URI'], '?', true);
if (strstr(__FILE__, $fileurl) || !$fileurl) { // Return 404 if the current file is called directly
    http_response_code(404);
    exit;
}
$filepath = realpath('./' . urldecode($fileurl));
$filetype = getImageTypeFromSrc($filepath);
$filebasename = basename($filepath);
preg_match("#^(.+)\.(jpe?g|png|gif)$#i", $filebasename, $matches);
$filename = $matches[1];
$fileext = $matches[2];
$dirpath = dirname($filepath) . "/";
$outputfilename = $filebasename.'?'.$_SERVER['REDIRECT_QUERY_STRING'];
if (strlen($outputfilename) > 255) {
    $outputfilename = $filebasename.'?imageprocessorSha='.sha1($_SERVER['REDIRECT_QUERY_STRING']);
}
$outputfilepath = $dirpath.$outputfilename;

// If matching params file exists, Return the image directly without any processing
if (is_file($outputfilepath)) {
    header("Content-type: image/".getImageTypeFromSrc($outputfilepath));
    echo file_get_contents($outputfilepath);
    exit;
}

// If no known param is given, Return the image directly without any processing
if (!isset($_GET['type']) &&
    !isset($_GET['quality']) &&
    !isset($_GET['maxw']) &&
    !isset($_GET['maxh']) &&
    !isset($_GET['minw']) &&
    !isset($_GET['minh']) &&
    !isset($_GET['size']) &&
    !isset($_GET['w']) &&
    !isset($_GET['h']) &&
    !isset($_GET['crop']) &&
    !isset($_GET['margins']) &&
    !isset($_GET['filter']) &&
    !isset($_GET['bgcolor']) &&
    !isset($_GET['nocache']) &&
    !isset($_GET['text']) &&
    !isset($_GET['uppertext']) &&
    !isset($_GET['lowertext']) 
) {
    header("Content-type: image/".$filetype);
    echo file_get_contents($filepath);
    exit;
}


// Get given params or Defaults
$type = isset($_GET['type']) ? strtolower($_GET['type']) : $filetype;
if ($type == 'jpg') {
    $type = 'jpeg';
}
$quality = isset($_GET['quality']) ? $_GET['quality'] : 90;
$maxw = isset($_GET['maxw']) ? $_GET['maxw'] : null;
$maxh = isset($_GET['maxh']) ? $_GET['maxh'] : null;
$minw = isset($_GET['minw']) ? $_GET['minw'] : null;
$minh = isset($_GET['minh']) ? $_GET['minh'] : null;
$size = isset($_GET['size']) ? $_GET['size'] : null;
if (isset($_GET['w'])) $size = $_GET['w'].'x';
if (isset($_GET['h'])) $size = 'x'.$_GET['h'];
$crop = isset($_GET['crop']) ? ($_GET['crop'] ? $_GET['crop'] : true) : false;
$margins = isset($_GET['margins']) ? ($_GET['margins'] ? $_GET['margins'] : true) : false;
$filters = isset($_GET['filter']) ? preg_split("#[\|, ;-]+#i", $_GET['filter']) : array();
$bgcolor = isset($_GET['bgcolor']) ? preg_split("#[\|, ;-]+#i", $_GET['bgcolor']) : ($type == 'png' ? null : array(255, 255, 255));
$nocache = isset($_GET['nocache']) ? true : false;
$texts = array();
foreach (array('text', 'uppertext', 'lowertext') as $key) {
    if (($text = isset($_GET[$key]) ? $_GET[$key] : '') !== '') {
        $textsize = !empty($_GET[$key.'size']) ? $_GET[$key.'size'] : '10-20';
        $textcolor = !empty($_GET[$key.'color']) ? preg_split("#[\|, ;-]+#i", $_GET[$key.'color']) : array(255, 255, 255);
        $textfont = !empty($_GET[$key.'font']) ? $_GET[$key.'font'] : 'OpenSans-Bold.ttf';
        $textalign = !empty($_GET[$key.'align']) ? $_GET[$key.'align'] : 'center';
        $texts[$key] = array(
            'text' => $text,
            'minsize' => (preg_match("#^(\d+)-(\d+)$#", $textsize, $matches) ? $matches[1] : $textsize),
            'maxsize' => (preg_match("#^(\d+)-(\d+)$#", $textsize, $matches) ? $matches[2] : $textsize),
            'color' => $textcolor,
            'font' => $textfont,
            'align' => $textalign,
        );
    }
}


// Create image object from original image
$image = imageCreateFromSrc($filepath);

// Check for error...
if (!$image) {
    throw new Exception("Error creating image object from input file");
}


// Set base params for processing
$outputWidth = $imageWidth = imagesx($image);
$outputHeight = $imageHeight = imagesy($image);
$imageRatio = $imageWidth / $imageHeight;


// Process Specific Size
if ($size) { // Exact output size given
    if (!preg_match("#^(\d*)x(\d*)$#i", $size, $matches)) { // (size=200x100)
        throw new Exception("invalid size param");
    }
    $outputWidth = (int)$matches[1];
    $outputHeight = (int)$matches[2];
    
    if ($outputWidth && !$outputHeight) { // Automatic height based on original image ratio if only width is given (size=200x) (w=200)
        $outputHeight = $outputWidth / $imageRatio;
    } else if (!$outputWidth && $outputHeight) { // Automatic width based on original image ratio if only height is given (size=x100) (h=100)
        $outputWidth = $outputHeight * $imageRatio;
    } else if (!$outputWidth && !$outputHeight) { // (size=x)
        $outputWidth = $imageWidth;
        $outputHeight = $imageHeight;
    }
}


// Max Width
if ($maxw && $outputWidth > $maxw) {
    $outputWidth = $maxw;
    if (!$size) {
        $outputHeight = $outputWidth / $imageRatio;
    }
}

// Max Height
if ($maxh && $outputHeight > $maxh) {
    $outputHeight = $maxh;
    if (!$size) {
        $outputWidth = $outputHeight * $imageRatio;
    }
}

// Min Width
if ($minw && $outputWidth < $minw) {
    $outputWidth = $minw;
    if (!$size) {
        $outputHeight = $outputWidth / $imageRatio;
        if ($maxh && $outputHeight > $maxh) {
            $outputHeight = $maxh;
        }
    }
}

// Min Height
if ($minh && $outputHeight < $minh) {
    $outputHeight = $minh;
    if (!$size) {
        $outputWidth = $outputHeight * $imageRatio;
        if ($maxw && $outputWidth > $maxw) {
            $outputWidth = $maxw;
        }
    }
}


// Initial fit without crop or margins
$dst_x = 0;
$dst_y = 0;
$src_x = 0;
$src_y = 0;
$dst_w = $outputWidth;
$dst_h = $outputHeight;
$src_w = $imageWidth;
$src_h = $imageHeight;

$outputRatio = $outputWidth / $outputHeight;

// Preventing distortion by cropping or by adding margins
if ($crop || $margins) {
    if ($crop) {
        if ($outputRatio > $imageRatio) { // output size is wider than original image, will crop on top and bottom, fit by width
            $resizeRatio = $outputWidth / $imageWidth;
        } else { // output size is higher than original image, will crop on sides, fit by height
            $resizeRatio = $outputHeight / $imageHeight;
        }
    } else if ($margins) {
        if ($outputRatio > $imageRatio) { // output size is wider than original image, margins would be on the sides, fit by height
            $resizeRatio = $outputHeight / $imageHeight;
        } else { // output size is higher than original image, margins would be on top and bottom, fit by width
            $resizeRatio = $outputWidth / $imageWidth;
        }
    }
    
    // Set destination width and height in output image
    $dst_w = $imageWidth * $resizeRatio;
    $dst_h = $imageHeight * $resizeRatio;

    // Get differences of width and height between destination cut and total output image
    $diffw = $outputWidth - $dst_w;
    $diffh = $outputHeight - $dst_h;

    // Set Crop/Margins for a centered image
    $dst_x = $diffw / 2;
    $dst_y = $diffh / 2;
    
    // Position the image if a certain direction is given
    $direction = (string) ($crop ? $crop : $margins);
    if (strlen($direction) > 1) {
        
        if (strpos($direction, 'top') !== false) { // Align Bottom, crop or add margins on top
            $dst_y = $diffh;
        }
        
        if (strpos($direction, 'bottom') !== false) { // Align Top, crop or add margins on the bottom
            $dst_y = 0;
        }
        
        if (strpos($direction, 'left') !== false) { // Align Right, crop or add margins to the left
            $dst_x = $diffw;
        }
        
        if (strpos($direction, 'right') !== false) { // Align Left, crop or add margins to the right
            $dst_x = 0;
        }
        
        if (preg_match("#^(-?\d+)[x,;\|/ -]+(-?\d+)$#i", $direction, $matches)) { // Align to a given position x,y relative to the output size
            $dst_x = $matches[1];
            $dst_y = $matches[2];
        }
    }
}


// Create new image
$outputImage = imagecreatetruecolor($outputWidth, $outputHeight);

// Fill background color
if ($bgcolor) {
    imagefill($outputImage, 0, 0, imagecolorallocate($outputImage, $bgcolor[0], $bgcolor[1], $bgcolor[2]));
} else {
    // Transparent PNG
    imagealphablending($outputImage, false);
    $transparency = imagecolorallocatealpha($outputImage, 0, 0, 0, 127);
    imagefill($outputImage, 0, 0, $transparency);
    imagesavealpha($outputImage, true);
}

// Process image
imagecopyresampled($outputImage, $image, round($dst_x), round($dst_y), round($src_x), round($src_y), round($dst_w), round($dst_h), round($src_w), round($src_h));
imagealphablending($outputImage, true);

// Destroy old image object
imagedestroy($image);

// Apply filters
foreach ($filters as $filter) {
    switch ($filter) {
        case 'grayscale' : case 'greyscale' : 
            imagefilter($outputImage, IMG_FILTER_GRAYSCALE);
            break;
    }
}

// Texts
foreach ($texts as $key => $textinfo) {
    $text = $textinfo['text'];
    
    // Get Text Bounding Box, Calculate Font Size
    $textSize = $textinfo['maxsize'] + 1;
    do {
        $textSize--;
        if ($textSize < $textinfo['minsize'] && !empty($textBbox) && $textBbox['height'] < $outputHeight) {
            $textSize = $textinfo['minsize'];
            // Multiline
            $lines = array();
            $availableWords = preg_split("#\s+#", $text);
            while ($availableWords) {
                $words = $availableWords;
                $availableWords = array();
                $lineBbox = null;
                do {
                    if (count($words) > 1 && $lineBbox) {
                        array_unshift($availableWords, array_pop($words));
                    }
                    $line = implode(' ', $words);
                    $lineBbox = getTextBoundingBox($line, $textinfo['font'], $textSize);
                } while ($lineBbox['width'] > $outputWidth && count($words) > 1);
                $lines[] = $line;
            }
            $text = implode("\n", $lines);
        }
        $textBbox = getTextBoundingBox($text, $textinfo['font'], $textSize);
    } while ($textBbox['width'] > $outputWidth || $textBbox['height'] > $outputHeight);
    
    // Vertical Align
    switch ($key) {
        case 'text' : 
            // Middle
            $y = ($outputHeight - $textBbox['height']) / 2;
            break;
        case 'uppertext' : 
            // Top
            $y = 4;
            break;
        case 'lowertext' : 
            // Bottom
            $y = $outputHeight - $textBbox['height'];
            break;
    }
    
    addTextToImage($outputImage, $text, $textinfo['font'], $textSize, $textinfo['color'], 0, $y, $outputWidth, $textinfo['align']);
}


// Prepare output header
header("Content-type: image/".$type);

// If nocache param is given, do not save the file
if ($nocache) {
    $outputfilepath = null;
}

// Save new image to a file (or output it directly if nocache param is given)
switch ($type) {
    case 'jpeg' :
        imagejpeg($outputImage, $outputfilepath, $quality);
        break;
    case 'png' : 
        imagepng($outputImage, $outputfilepath, 0);
        break;
    case 'gif' : 
        imagegif($outputImage, $outputfilepath);
        break;
    default : throw new Exception("Invalid output file type");
}

// Destroy new image object
imagedestroy($outputImage);

// Output newly created image file
if (!$nocache) {
    echo file_get_contents($outputfilepath);
}

exit;
